/*
 * Copyright © 2021 Dowsure
 * https://www.dowsure.com/
 *
 * All rights reserved.
 */

package com.dowsure.apisaas.util;

import com.dowsure.apisaas.enums.MethodEnums;
import com.dowsure.apisaas.exception.ApiException;
import com.dowsure.apisaas.util.smalgorithm.SM2Util;
import com.dowsure.apisaas.util.smalgorithm.SM4KeyPair;
import com.dowsure.apisaas.util.smalgorithm.SM4Util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.squareup.okhttp.*;
import io.gsonfire.GsonFireBuilder;
import okio.BufferedSink;
import okio.Okio;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.threeten.bp.LocalDate;
import org.threeten.bp.OffsetDateTime;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @program: dowsure-apis
 * @description:
 * @author: Dowsure
 * @create: 2021-12-09 11:46
 **/
public class ApiClient {

    /**
     * 接口服务地址
     */
    private String basePath;

    /**
     * 机构号（由Dowsure提供）
     */
    private String organizationCode;

    /**
     * dowsure secret key 由Dowsure提供的密钥
     */
    private String dowsureSecretKey;

    /**
     * partner secret key （由合作方生成的sm2私钥）
     */
    private String partnerSecretKey;

    /**
     * default headers
     */
    private Map<String, String> defaultHeaderMap = new HashMap<>();

    /**
     * httpClient
     */
    private OkHttpClient httpClient;

    /**
     * gson
     */
    private Gson gson;

    /**
     * sm4密钥
     */
    private SM4KeyPair sm4KeyPair;

    /**
     * 版本
     */
    private String version = "v1";

    /**
     * 临时文件路径
     */
    private String tempFolderPath;

    /**
     * 编码
     */
    private static final String ENCODING = "UTF-8";

    /**
     * set base path.
     *
     * @param basePath base path.
     * @return ApiClient
     */
    public ApiClient setBasePath(String basePath) {
        this.basePath = basePath;
        return this;
    }


    /**
     * set organization code.
     *
     * @param organizationCode organization code.
     * @return ApiClient
     */
    public ApiClient setOrganizationCode(String organizationCode) {
        this.organizationCode = organizationCode;
        return this;
    }

    /**
     * set dowsure secret key.
     *
     * @param dowsureSecretKey dowsure secret key. SM2 public key given by dowsure
     * @return ApiClient
     */
    public ApiClient setDowsureSecretKey(String dowsureSecretKey) {
        this.dowsureSecretKey = dowsureSecretKey;
        return this;
    }

    /**
     * set partner secret key.
     *
     * @param partnerSecretKey partner secret key Partner's SM2 private key
     * @return ApiClient
     */
    public ApiClient setPartnerSecretKey(String partnerSecretKey) {
        this.partnerSecretKey = partnerSecretKey;
        return this;
    }

    /**
     * Add a default header.
     *
     * @param key The header's key
     * @param value The header's value
     * @return ApiClient
     */
    public ApiClient addDefaultHeader(String key, String value) {
        defaultHeaderMap.put(key, value);
        return this;
    }

    /**
     * set version number.
     * @param version version number
     * @return ApiClient
     */
    public ApiClient setVersion(String version) {
        this.version = version;
        return this;
    }

    /**
     * get version number.
     * @return String (version number)
     */
    public String getVersion() {
        return version;
    }


    public ApiClient(String basePath, String organizationCode, String dowsureSecretKey, String partnerSecretKey){
        this.basePath = basePath;
        GsonFireBuilder fireBuilder = new GsonFireBuilder();
        GsonBuilder builder = fireBuilder.createGsonBuilder();
        this.gson = builder.create();

        this.httpClient = new OkHttpClient();
        this.organizationCode = organizationCode;
        this.dowsureSecretKey = dowsureSecretKey;
        this.partnerSecretKey = partnerSecretKey;
    }

    public Call buildCall(String path, MethodEnums method, Object body, Map<String, String> headerParams) throws ApiException {
        Request request = buildRequest(path, method, body, headerParams);

        //临时打印
        System.err.println("参数明文： " + gson.toJson(body));
        System.err.println("url： " + request.urlString());
        System.err.println("------ header start ------");
        Headers headers = request.headers();
        for (int i = 0; i< headers.size(); i++){
            String name = headers.name(i);
            System.err.println(name + " :  " + headers.get(name));
        }
        System.err.println("------ header end ------");

        return httpClient.newCall(request);
    }

    public Request buildRequest(String path, MethodEnums method, Object params, Map<String, String> headerParams) throws ApiException {
        //encryption
        Map<String, String> mapParams = processContent(method, params);

        //临时打印
        System.err.println("参数加密后： " + mapParams.toString());

        //url
        String url = buildUrl(method, path, mapParams);

        Request.Builder reqBuilder = new Request.Builder().url(url);

        //header
        processHeaderParams(headerParams, reqBuilder);

        Request request = null;
        switch (method){
            case GET:

                request = reqBuilder.get().build();
                break;
            case POST:
                String contentType = headerParams.get("Content-Type");
                // ensuring a default content type
                if (contentType == null) {
                    contentType = "application/json";
                }
                RequestBody reqBody = RequestBody.create(MediaType.parse(contentType), gson.toJson(mapParams));
                request = reqBuilder.post(reqBody).build();
                break;
            case DELETE:
                //void
                break;
            default:
                break;
        }

        return request;
    }



    /**
     * Set header parameters to the request builder, including default headers.
     *
     * @param headerParams Header parameters in the ofrm of Map
     * @param reqBuilder Reqeust.Builder
     */
    public void processHeaderParams(Map<String, String> headerParams, Request.Builder reqBuilder) throws ApiException {
        for (Map.Entry<String, String> header : defaultHeaderMap.entrySet()) {
            if (!headerParams.containsKey(header.getKey())) {
                reqBuilder.header(header.getKey(), parameterToString(header.getValue()));
            }
        }
        for (Map.Entry<String, String> param : headerParams.entrySet()) {
            reqBuilder.header(param.getKey(), parameterToString(param.getValue()));
        }

        try {
            reqBuilder.addHeader("clientid", organizationCode);
            reqBuilder.addHeader("sm4key", Base64.getEncoder().encodeToString(SM2Util.encrypt(Base64.getDecoder().decode(dowsureSecretKey), sm4KeyPair.getSecretKeyByte())));
            reqBuilder.addHeader("sm4iv", Base64.getEncoder().encodeToString(SM2Util.encrypt(Base64.getDecoder().decode(dowsureSecretKey), sm4KeyPair.getIvByte())));
            reqBuilder.addHeader("timestamp", String.valueOf(System.currentTimeMillis()));
        } catch (InvalidCipherTextException e) {
            e.printStackTrace();
            throw new ApiException(e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            throw new ApiException(e.getMessage());
        }

    }

    public Map<String, String> processContent(MethodEnums method, Object params) throws ApiException {

        this.sm4KeyPair = SM4Util.generateSecretKey(16);

        Map<String, String> paramsMap = new HashMap<>();
        String jsonParams = gson.toJson(params);
        String ciphertext = processParams(jsonParams);
        String sign = processSign(jsonParams);

        switch (method){
            case GET:
                try {
                    paramsMap.put("content", URLEncoder.encode(ciphertext, ENCODING));
                    paramsMap.put("sign", URLEncoder.encode(sign, ENCODING));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                break;
            case POST:
                paramsMap.put("content", ciphertext);
                paramsMap.put("sign", sign);
                break;
        }
        return paramsMap;
    }

    public String processSign(String jsonParams) throws ApiException {
        try {
            return Base64.getEncoder().encodeToString(SM2Util.sign(Base64.getDecoder().decode(partnerSecretKey), jsonParams.getBytes(ENCODING)));
        } catch (CryptoException e) {
            throw new ApiException(e.getMessage());
        } catch (IOException e) {
            throw new ApiException(e.getMessage());
        }
    }

    public String processParams(String jsonParams){
        //encryption params
        return Base64.getEncoder().encodeToString(SM4Util.encryptCBC(jsonParams.getBytes(), sm4KeyPair.getSecretKeyByte(), sm4KeyPair.getIvByte()));
    }

    /**
     * Format the given parameter object into string.
     *
     * @param param Parameter
     * @return String representation of the parameter
     */
    public String parameterToString(Object param) {
        if (param == null) {
            return "";
        } else if (param instanceof Date || param instanceof OffsetDateTime || param instanceof LocalDate) {
//        } else if (param instanceof Date || param instanceof OffsetDateTime || param instanceof LocalDate) {
            //Serialize to json string and remove the " enclosing characters
            GsonFireBuilder fireBuilder = new GsonFireBuilder();
            GsonBuilder builder = fireBuilder.createGsonBuilder();
            Gson gson = builder.create();
            String jsonStr = gson.toJson(param);
            return jsonStr.substring(1, jsonStr.length() - 1);
        } else if (param instanceof Collection) {
            StringBuilder b = new StringBuilder();
            for (Object o : (Collection)param) {
                if (b.length() > 0) {
                    b.append(",");
                }
                b.append(o);
            }
            return b.toString();
        } else {
            return String.valueOf(param);
        }
    }

    public String buildUrl(MethodEnums method, String path, Map<String, String> mapParams){
        StringBuilder url = new StringBuilder();
        url.append(basePath).append(path);
        if (method.equals(MethodEnums.GET)){
            url.append("?");
            url.append("content=").append(mapParams.get("content"));
            url.append("&");
            url.append("sign=").append(mapParams.get("sign"));
        }
        return url.toString();
    }

    /**
     * Check if the given MIME is a JSON MIME.
     * JSON MIME examples:
     *   application/json
     *   application/json; charset=UTF8
     *   APPLICATION/JSON
     *   application/vnd.company+json
     * "* / *" is also default to JSON
     * @param mime MIME (Multipurpose Internet Mail Extensions)
     * @return True if the given MIME is JSON, false otherwise.
     */
    public boolean isJsonMime(String mime) {
        String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$";
        return mime != null && (mime.matches(jsonMime) || mime.equals("*/*"));
    }

    /**
     * Execute HTTP call asynchronously.
     *
     * @param call The callback to be executed when the API call finishes
     * @param returnType Return type
     * @param callback ApiCallback
     */
    @SuppressWarnings("unchecked")
    public <T> void executeAsync(Call call, final Type returnType, final ApiCallback<T> callback) {
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                callback.onFailure(new ApiException(e), 0, null);
            }

            @Override
            public void onResponse(Response response) throws IOException {
                T result;
                try {
                    result = (T) handleResponse(response, returnType);
                } catch (ApiException e) {
                    callback.onFailure(e, response.code(), response.headers().toMultimap());
                    return;
                }
                callback.onSuccess(result, response.code(), response.headers().toMultimap());
            }
        });
    }

    /**
     * 处理给定的响应，当响应成功时返回反序列化对象。
     * @param response Response
     * @param returnType Return type
     * @throws ApiException If the response has a unsuccessful status code or
     *   fail to deserialize the response body
     * @return Type
     */
    public <T> T handleResponse(Response response, Type returnType) throws ApiException {
        if (response.isSuccessful()) {
            if (returnType == null || response.code() == 204) {
                // returning null if the returnType is not defined,
                // or the status code is 204 (No Content)
                if (response.body() != null) {
                    try {
                        response.body().close();
                    } catch (IOException e) {
                        throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap());
                    }
                }
                return null;
            } else {
                return deserialize(response, returnType);
            }
        } else {
            String respBody = null;
            if (response.body() != null) {
                try {
                    respBody = response.body().string();
                    ApiResponse<String> result = gson.fromJson(respBody, ApiResponse.class);
                    throw new ApiException(result.getMessage(), result.getCode(), response.headers().toMultimap(), result.getData());
                } catch (IOException e) {
                    throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap());
                }
            }
            throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), respBody);
        }
    }

    /**
     * Deserialize response body to Java object, according to the return type and
     * the Content-Type response header.
     *
     * @param response HTTP response
     * @param returnType The type of the Java object
     * @return The deserialized Java object
     * @throws ApiException If fail to deserialize response body, i.e. cannot read response body
     *   or the Content-Type of the response is not supported.
     */
    @SuppressWarnings("unchecked")
    public <T> T deserialize(Response response, Type returnType) throws ApiException {
        if (response == null || returnType == null) {
            return null;
        }

        if ("byte[]".equals(returnType.toString())) {
            // Handle binary response (byte array).
            try {
                return (T) response.body().bytes();
            } catch (IOException e) {
                throw new ApiException(e);
            }
        } else if (returnType.equals(File.class)) {
            // Handle file downloading.
            return (T) downloadFileFromResponse(response);
        }

        String respBody;
        try {
            if (response.body() != null) {
                respBody = response.body().string();
            } else {
                respBody = null;
            }
        } catch (IOException e) {
            throw new ApiException(e);
        }

        if (respBody == null || "".equals(respBody)) {
            return null;
        }
        String contentType = response.headers().get("Content-Type");
        if (contentType == null) {
            // ensuring a default content type
            contentType = "application/json";
        }
        if (isJsonMime(contentType)) {
            return gson.fromJson(respBody, returnType);
//            return gson.deserialize(respBody, returnType);
        } else if (returnType.equals(String.class)) {
            // Expecting string, return the raw response body.
            return (T) respBody;
        } else {
            throw new ApiException(
                    "Content type \"" + contentType + "\" is not supported for type: " + returnType,
                    response.code(),
                    response.headers().toMultimap(),
                    respBody);
        }
    }

    /**
     * Download file from the given response.
     *
     * @param response An instance of the Response object
     * @throws ApiException If fail to read file content from response and write to disk
     * @return Downloaded file
     */
    public File downloadFileFromResponse(Response response) throws ApiException {
        try {
            File file = prepareDownloadFile(response);
            BufferedSink sink = Okio.buffer(Okio.sink(file));
            sink.writeAll(response.body().source());
            sink.close();
            return file;
        } catch (IOException e) {
            throw new ApiException(e);
        }
    }

    /**
     * Prepare file for download
     *
     * @param response An instance of the Response object
     * @throws IOException If fail to prepare file for download
     * @return Prepared file for the download
     */
    public File prepareDownloadFile(Response response) throws IOException {
        String filename = null;
        String contentDisposition = response.header("Content-Disposition");
        if (contentDisposition != null && !"".equals(contentDisposition)) {
            // Get filename from the Content-Disposition header.
            Pattern pattern = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
            Matcher matcher = pattern.matcher(contentDisposition);
            if (matcher.find()) {
                filename = sanitizeFilename(matcher.group(1));
            }
        }

        String prefix = null;
        String suffix = null;
        if (filename == null) {
            prefix = "download-";
            suffix = "";
        } else {
            int pos = filename.lastIndexOf(".");
            if (pos == -1) {
                prefix = filename + "-";
            } else {
                prefix = filename.substring(0, pos) + "-";
                suffix = filename.substring(pos);
            }
            // File.createTempFile requires the prefix to be at least three characters long
            if (prefix.length() < 3) {
                prefix = "download-";
            }
        }

        if (tempFolderPath == null){
            return File.createTempFile(prefix, suffix);
        } else{
            return File.createTempFile(prefix, suffix, new File(tempFolderPath));
        }
    }


    /**
     * Sanitize filename by removing path.
     * e.g. ../../sun.gif becomes sun.gif
     *
     * @param filename The filename to be sanitized
     * @return The sanitized filename
     */
    public String sanitizeFilename(String filename) {
        return filename.replaceAll(".*[/\\\\]", "");
    }

    /**
     * Execute HTTP call and deserialize the HTTP response body into the given return type.
     *
     * @param call Call
     * @return ApiResponse object containing response status, headers and
     *   data, which is a Java object deserialized from response body and would be null
     *   when returnType is null.
     * @throws ApiException If fail to execute the call
     */
    public <T> ApiResponse<T> execute(Call call) throws ApiException {
        try {
            Response response = call.execute();
            Headers headers = response.headers();

            Type ResultReturnType = new TypeToken<Map<String, String>>(){}.getType();
            Map<String, String> resultMap = handleResponse(response, ResultReturnType);
            String content = resultMap.get("content");
            String sign = resultMap.get("sign");

            byte[] sm4key = SM2Util.decrypt(Base64.getDecoder().decode(partnerSecretKey), Base64.getDecoder().decode(headers.get("sm4key")));
            byte[] sm4iv = SM2Util.decrypt(Base64.getDecoder().decode(partnerSecretKey), Base64.getDecoder().decode(headers.get("sm4iv")));

            byte[] dataByte = SM4Util.decryptCBC(Base64.getDecoder().decode(content), sm4key, sm4iv);
            ApiResponse<T> result = gson.fromJson(new String(dataByte, ENCODING), ApiResponse.class); //关键
            boolean verifySign = SM2Util.verifySign(Base64.getDecoder().decode(dowsureSecretKey), dataByte, Base64.getDecoder().decode(sign));
            if (verifySign){
                return new ApiResponse<T>(result.getCode(), response.headers().toMultimap(), result.getData(), result.getMsg());
            } else {
                throw new ApiException("response signature verification failed");
            }

        } catch (IOException | InvalidCipherTextException e) {
            throw new ApiException(e);
        }
    }

    /**
     * Execute HTTP call and deserialize the HTTP response body into the given return type.
     *
     * @param call Call
     * @return ApiResponse object containing response status, headers and
     *   data, which is a Java object deserialized from response body and would be null
     *   when returnType is null.
     * @throws ApiException If fail to execute the call
     */
    public <T> ApiResponse<T> executeTest(Call call) throws ApiException {
        try {
//            Response response = call.execute();
//            Headers headers = response.headers();
//            Base64.getEncoder().encodeToString(SM2Util.encrypt(Base64.getDecoder().decode(dowsureSecretKey), sm4KeyPair.getSecretKeyByte()));
            SM4KeyPair resultSm4KeyPair = SM4Util.generateSecretKey(16);
            Map<String, String> headerMap = new HashMap<>();
            headerMap.put("sm4key", resultSm4KeyPair.getSecretKey());
            headerMap.put("sm4iv", resultSm4KeyPair.getIv());
            headerMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
            Headers headers = Headers.of(headerMap);

//            Type ResultReturnType = new TypeToken<Map<String, String>>(){}.getType();
//            Map<String, String> resultMap = handleResponse(response, ResultReturnType);
//            String content = resultMap.get("content");
//            String sign = resultMap.get("sign");

//            byte[] sm4key = SM2Util.decrypt(Base64.getDecoder().decode(partnerSecretKey), Base64.getDecoder().decode(headers.get("sm4key")));
//            byte[] sm4iv = SM2Util.decrypt(Base64.getDecoder().decode(partnerSecretKey), Base64.getDecoder().decode(headers.get("sm4iv")));
//
//            byte[] dataByte = SM4Util.decryptCBC(Base64.getDecoder().decode(content), sm4key, sm4iv);
//            ApiResponse<T> result = gson.fromJson(new String(dataByte, ENCODING), ApiResponse.class); //关键
//            boolean verifySign = SM2Util.verifySign(Base64.getDecoder().decode(dowsureSecretKey), dataByte, Base64.getDecoder().decode(sign));
//            if (verifySign){
                return new ApiResponse<T>(200, headers.toMultimap(), null, "success", true);
//            } else {
//                throw new ApiException("response signature verification failed");
//            }

        } catch (Exception e) {
            throw new ApiException(e);
        }
    }

    /**
     * Serialize the given Java object into request body according to the object's
     * class and the request Content-Type.
     *
     * @param obj The Java object
     * @param contentType The request Content-Type
     * @return The serialized request body
     * @throws ApiException If fail to serialize the given object
     */
    public RequestBody serialize(Object obj, String contentType) throws ApiException {
        if (obj instanceof byte[]) {
            // Binary (byte array) body parameter support.
            return RequestBody.create(MediaType.parse(contentType), (byte[]) obj);
        } else if (obj instanceof File) {
            // File body parameter support.
            return RequestBody.create(MediaType.parse(contentType), (File) obj);
        } else if (isJsonMime(contentType)) {
            String content;
            if (obj != null) {
                content = gson.toJson(obj);
            } else {
                content = null;
            }
            return RequestBody.create(MediaType.parse(contentType), content);
        } else {
            throw new ApiException("Content type \"" + contentType + "\" is not supported");
        }
    }
}
